/*
 * MIT License
 * 
 * Copyright (c) 2024-2025 milkpotatoes
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <signal.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <unistd.h>
#include <uv.h>


#include "napi/native_api.h" // IWYU pragma: keep
#include "log.h"
#include "napi_utils.h"
#include "utils.h"

#include "include/js_process.h"

std::atomic<int> JsProcess::exitCode_ = 0;

__attribute__((noreturn)) napi_value JsProcess::Exit(napi_env env, napi_callback_info info_)
{
    utils::NapiCallbackInfo info(env, info_);
    if (info.GetArgc() > 0)
    {
        exit(utils::GetValueInt32(env, info[0]));
    }
    exit(exitCode_.load());
}

__attribute__((noreturn)) napi_value JsProcess::Abort(napi_env, napi_callback_info)
{
    abort();
}

napi_value JsProcess::Cwd(napi_env env, napi_callback_info)
{
    return utils::CreateStringUtf8(env, utils::GetCwd().c_str());
}

napi_value JsProcess::ChDir(napi_env env, napi_callback_info info_)
{
    utils::NapiCallbackInfo info(env, info_);
    napi_value path = info[0];
    napi_valuetype type = utils::TypeOf(env, path);
    std::string errMsg = "The \"directory\" must be of type string. ";
    switch (type)
    {
    case napi_string: {
        std::string to = utils::GetValueStringUtf8(env, path);
        int ret = utils::ChDir(to.c_str());
        if (ret != 0)
        {
            const char *code = strerror(ret);
            std::string msg = code;
            msg += ": no such file or directory, chdir '";
            msg += utils::GetCwd();
            msg += "' -> ";
            msg += to;
            napi_throw_error(env, code, msg.c_str());
        }
        return nullptr;
        break;
    }
    case napi_undefined:
        errMsg += "Received undefined";
        break;
    case napi_null:
        errMsg += "Received null";
        break;
    case napi_number:
        errMsg += "Received type number (" + std::to_string(utils::GetValueInt32(env, path)) + ")";
        break;
    case napi_boolean: {
        errMsg += "Received function";
        break;
    }
    case napi_function: {
        bool val = false;
        napi_get_value_bool(env, path, &val);
        if (val)
            errMsg += "Received type boolean (true)";
        else
            errMsg += "Received type boolean (false)";
        break;
    }
    case napi_external:
    case napi_object: {
        napi_value cons = utils::GetProperty(env, path, "constructor");
        errMsg += "Received an instance of " + utils::GetValueStringUtf8(env, utils::GetProperty(env, cons, "name"));
        break;
    }
    case napi_symbol: {
        napi_value toStr = utils::GetProperty(env, path, "toString");
        napi_value str = nullptr;
        napi_call_function(env, path, toStr, 0, nullptr, &str);
        errMsg += "Received Received type symbol (" + utils::GetValueStringUtf8(env, str) + ")";
        break;
    }
    case napi_bigint: {
        int64_t val = 0;
        bool loss = false;
        napi_get_value_bigint_int64(env, path, &val, &loss);
        errMsg += "Received Received type bigint (" + std::to_string(val) + "n)";
    }
    default:
        errMsg += "Received unknown";
        break;
    }
    utils::ThrowInvalidArg(env, errMsg.c_str());
    return nullptr;
}

napi_value JsProcess::GetArch(napi_env env, napi_callback_info)
{
    const char *arch = ""
#if defined(__aarch64__)
                       "arm64"
#elif defined(__arm__)
                       "arm"
#elif defined(__i386__)
                       "ia32"
#elif defined(__x86_64__) || defined(__amd64__)
                       "x64"
#else
                       "unknown"
#endif
        ;
    return utils::CreateStringUtf8(env, arch);
}

napi_value JsProcess::SetExitCode(napi_env env, napi_callback_info info_)
{
    utils::NapiCallbackInfo info(env, info_);
    exitCode_ = utils::GetValueInt32(env, info[0]);
    return nullptr;
}

napi_value JsProcess::GetPid(napi_env env, napi_callback_info)
{
    static pid_t pid = getpid();
    return utils::CreateUint32(env, pid);
}

napi_value JsProcess::GetPPid(napi_env env, napi_callback_info)
{
    static pid_t ppid = getppid();
    return utils::CreateUint32(env, ppid);
}

napi_value JsProcess::GetEGid(napi_env env, napi_callback_info)
{
    static gid_t egid = getegid();
    return utils::CreateInt32(env, egid);
}

napi_value JsProcess::GetGid(napi_env env, napi_callback_info)
{
    static gid_t gid = getgid();
    return utils::CreateInt32(env, gid);
}

napi_value JsProcess::GetGroups(napi_env env, napi_callback_info)
{
    int groupCounts = getgroups(0, nullptr);
    std::vector<gid_t> groups(groupCounts);
    getgroups(groups.size(), groups.data());
    napi_value result = utils::CreateArray(env);
    for (size_t i = 0; i < groups.size(); i++)
    {
        napi_set_element(env, result, i, utils::CreateInt32(env, groups[i]));
    }
    return result;
}

napi_value JsProcess::GetEUid(napi_env env, napi_callback_info)
{
    static uid_t euid = geteuid();
    return utils::CreateInt32(env, euid);
}

napi_value JsProcess::GetUid(napi_env env, napi_callback_info)
{
    static uid_t uid = getuid();
    return utils::CreateInt32(env, uid);
}

napi_value JsProcess::InnerKill(napi_env env, napi_callback_info info_)
{
    utils::NapiCallbackInfo info(env, info_);
    int32_t pid = utils::GetValueInt32(env, info[0]);
    int32_t sig = utils::GetValueInt32(env, info[1]);
    int ret = kill(pid, sig);
    return utils::CreateInt32(env, ret);
}

napi_value JsProcess::InnerGetEnviron(napi_env env, napi_callback_info info_)
{
    napi_value result = utils::CreateArray(env);
    for (int i = 0; environ[i] != nullptr; i++)
    {
        napi_set_element(env, result, i, utils::CreateStringUtf8(env, environ[i]));
    }
    return result;
}

napi_value JsProcess::InnerGetEnv(napi_env env, napi_callback_info info_)
{
    utils::NapiCallbackInfo info(env, info_);
    char *ret = getenv(utils::GetValueStringUtf8(env, info[0]).c_str());
    if (ret == NULL)
        return nullptr;
    return utils::CreateStringUtf8(env, ret);
}

napi_value JsProcess::InnerSetEnv(napi_env env, napi_callback_info info_)
{
    utils::NapiCallbackInfo info(env, info_);
    return utils::CreateInt32(env, setenv(utils::GetValueStringUtf8(env, info[0]).c_str(),
                                          utils::GetValueStringUtf8(env, info[1]).c_str(), 1));
}

napi_value JsProcess::InnerUnsetEnv(napi_env env, napi_callback_info info_)
{
    utils::NapiCallbackInfo info(env, info_);
    return utils::CreateInt32(env, unsetenv(utils::GetValueStringUtf8(env, info[0]).c_str()));
}

napi_value JsProcess::InitSignalMap(napi_env env)
{
#define DEFINE_SIGNAL_PROP(sig) \
    {#sig, nullptr, nullptr, nullptr, nullptr, utils::CreateInt32(env, sig), napi_default_jsproperty, nullptr}
    napi_property_descriptor desc[] = {
#ifdef SIGINT
        DEFINE_SIGNAL_PROP(SIGINT),
#endif
#ifdef SIGILL
        DEFINE_SIGNAL_PROP(SIGILL),
#endif
#ifdef SIGABRT
        DEFINE_SIGNAL_PROP(SIGABRT),
#endif
#ifdef SIGFPE
        DEFINE_SIGNAL_PROP(SIGFPE),
#endif
#ifdef SIGSEGV
        DEFINE_SIGNAL_PROP(SIGSEGV),
#endif
#ifdef SIGTERM
        DEFINE_SIGNAL_PROP(SIGTERM),
#endif
#ifdef SIGHUP
        DEFINE_SIGNAL_PROP(SIGHUP),
#endif
#ifdef SIGQUIT
        DEFINE_SIGNAL_PROP(SIGQUIT),
#endif
#ifdef SIGTRAP
        DEFINE_SIGNAL_PROP(SIGTRAP),
#endif
#ifdef SIGKILL
        DEFINE_SIGNAL_PROP(SIGKILL),
#endif
#ifdef SIGPIPE
        DEFINE_SIGNAL_PROP(SIGPIPE),
#endif
#ifdef SIGALRM
        DEFINE_SIGNAL_PROP(SIGALRM),
#endif
#ifdef SIGIO
        DEFINE_SIGNAL_PROP(SIGIO),
#endif
#ifdef SIGIOT
        DEFINE_SIGNAL_PROP(SIGIOT),
#endif
#ifdef SIGCLD
        DEFINE_SIGNAL_PROP(SIGCLD),
#endif
    };
    napi_value result = utils::CreateObject(env);
    napi_define_properties(env, result, sizeof(desc) / sizeof(desc[0]), desc);
    return result;
#undef DEFINE_SIGNAL_PROP
}

JsProcess::UvPollEventsCallback::~UvPollEventsCallback()
{
    napi_delete_reference(env_, readable_);
    readable_ = nullptr;
    napi_delete_reference(env_, writable_);
    writable_ = nullptr;
    napi_delete_reference(env_, disconnect_);
    disconnect_ = nullptr;
    napi_delete_reference(env_, prioritized_);
    prioritized_ = nullptr;
    napi_delete_reference(env_, error_);
    error_ = nullptr;
}

void JsProcess::UvPollEventsCallback::Invoke(uv_poll_t *poll, int status, int events)
{
#define ABORT_IF_UNCAUGHT_EXCEPTION(callback)                                                                    \
    do                                                                                                           \
    {                                                                                                            \
        napi_value func = nullptr;                                                                               \
        if (callback##_ == nullptr || napi_get_reference_value(env_, callback##_, &func) != napi_ok)             \
            break;                                                                                               \
        if (napi_call_function(env_, global, func, args.size(), args.data(), nullptr) == napi_pending_exception) \
        {                                                                                                        \
            LogError("Uncaught exception pending after error " #callback " triggered");                          \
            abort();                                                                                             \
        }                                                                                                        \
    } while (0);

    utils::NapiHandleScope scope(env_);
    napi_value global = utils::GetGlobal(env_);
    uv_os_fd_t fd;
    if (uv_fileno(reinterpret_cast<uv_handle_t *>(poll), &fd) != 0)
        fd = -1;
    if (status != 0)
    {
        std::vector args{utils::CreateInt32(env_, status), utils::CreateInt32(env_, fd)};
        ABORT_IF_UNCAUGHT_EXCEPTION(error);
    }
    std::vector args{utils::CreateInt32(env_, fd)};
    if ((events & UV_READABLE) != 0)
        ABORT_IF_UNCAUGHT_EXCEPTION(readable);
    if ((events & UV_WRITABLE) != 0)
        ABORT_IF_UNCAUGHT_EXCEPTION(writable);
    if ((events & UV_DISCONNECT) != 0)
        ABORT_IF_UNCAUGHT_EXCEPTION(disconnect);
    if ((events & UV_PRIORITIZED) != 0)
        ABORT_IF_UNCAUGHT_EXCEPTION(prioritized);
}

napi_value JsProcess::InnerReadFd(napi_env env, napi_callback_info info_)
{
    utils::NapiCallbackInfo info(env, info_);
    int fd = utils::GetValueInt32(env, info[0]);
    if (fd < 0)
        return nullptr;
    uv_poll_t *poll = new uv_poll_t;
    auto callbacks = new UvPollEventsCallback(env);
    poll->data = callbacks;
    bool autoRelease = true;
    bool pollInit = false;
    utils::StackCaller caller([&autoRelease, poll, callbacks, &pollInit] {
        if (!autoRelease)
            return;
        if (pollInit)
        {
            uv_poll_stop(poll);
            uv_close(reinterpret_cast<uv_handle_t *>(poll),
                     [](uv_handle_t *handle) { delete reinterpret_cast<uv_poll_t *>(handle); });
        }
        else
        {
            delete poll;
        }
        delete callbacks;
    });
    int uvStatus;
#define UV_CALL_NO_ERROR(call)                                                 \
    uvStatus = (call);                                                         \
    if (uvStatus != 0)                                                         \
    {                                                                          \
        std::string msg = ("Failed to execute readFd: ");                      \
        napi_throw_error(env, nullptr, (msg + uv_strerror(uvStatus)).c_str()); \
        return nullptr;                                                        \
    }
    UV_CALL_NO_ERROR(uv_poll_init(utils::GetUVLoop(env), poll, fd));
    pollInit = true;
    size_t idx = 1;
    int event = 1;
    int events = 0;
    for (auto *ref = &callbacks->error_; ref <= &callbacks->prioritized_; ref++)
    {
        if (idx >= info.GetArgc())
            break;
        utils::StackCaller caller([&idx, &event] {
            if (idx > 1)
                event <<= 1;
            idx++;
        });
        if (utils::TypeOf(env, info[idx]) != napi_function)
            continue;
        events |= event;
        napi_create_reference(env, info[idx], 1, ref);
    }
    UV_CALL_NO_ERROR(uv_poll_start(poll, events, [](uv_poll_t *poll, int status, int events) {
        reinterpret_cast<UvPollEventsCallback *>(poll->data)->Invoke(poll, status, events);
    }));
#undef UV_CALL_NO_ERROR
    napi_value result = nullptr;
    napi_create_external(
        env, reinterpret_cast<void *>(poll),
        [](napi_env, void *data, void *hint) {
            delete reinterpret_cast<UvPollEventsCallback *>(hint);
            auto poll = reinterpret_cast<uv_poll_t *>(data);
            poll->data = nullptr;
            uv_poll_stop(poll);
            uv_close(reinterpret_cast<uv_handle_t *>(poll),
                     [](uv_handle_t *handle) { delete reinterpret_cast<uv_poll_t *>(handle); });
        },
        reinterpret_cast<void *>(callbacks), &result);
    autoRelease = false;
    return result;
}

napi_value JsProcess::InnerGetTitle(napi_env env, [[maybe_unused]] napi_callback_info info)
{
    char name[17] = {0}; // 17: max proocess name and NULL
    if (prctl(PR_GET_NAME, name) == 0)
        return utils::CreateStringUtf8(env, name);
    return nullptr;
}

napi_value JsProcess::InnerSetTitle(napi_env env, napi_callback_info info)
{
    utils::NapiCallbackInfo cbinfo(env, info);
    std::string name = utils::GetValueStringUtf8(env, cbinfo[0]);
    prctl(PR_SET_NAME, name.c_str());
    return nullptr;
}

napi_value JsProcess::Init(napi_env env, napi_value global, int argc, int index, const char *argv[])
{
    napi_value execPath = utils::CreateStringUtf8(env, utils::GetRealPath(argv[0]).c_str());
    napi_value jsArgv = utils::CreateArray(env);
    napi_value execArgv = utils::CreateArray(env);
    napi_set_element(env, jsArgv, 0, execPath);
    for (int i = 1; i < index; i++)
    {
        napi_value arg = utils::CreateStringUtf8(env, argv[i]);
        napi_set_element(env, execArgv, i - 1, arg);
    }
    napi_set_element(env, jsArgv, 1, utils::CreateStringUtf8(env, utils::GetRealPath(argv[index]).c_str()));
    for (int i = index + 1, j = 2; i < argc; i++, j++)
    {
        napi_value arg = utils::CreateStringUtf8(env, argv[i]);
        napi_set_element(env, jsArgv, j, arg);
    }
    std::vector<napi_property_descriptor> desc{
        {"exit", nullptr, Exit, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"abort", nullptr, Abort, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"arch", nullptr, nullptr, GetArch, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"cwd", nullptr, Cwd, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"chdir", nullptr, ChDir, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"argv", nullptr, nullptr, nullptr, nullptr, jsArgv, napi_default_jsproperty, nullptr},
        {"execPath", nullptr, nullptr, nullptr, nullptr, execPath, napi_default_jsproperty, nullptr},
        {"execArgv", nullptr, nullptr, nullptr, nullptr, execArgv, napi_default_jsproperty, nullptr},
        {"pid", nullptr, nullptr, GetPid, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"ppid", nullptr, nullptr, GetPPid, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"getegid", nullptr, GetEGid, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"geteuid", nullptr, GetEUid, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"getgid", nullptr, GetGid, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"getgroups", nullptr, GetGroups, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"getuid", nullptr, GetUid, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_setExitCode", nullptr, SetExitCode, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_signalMaps", nullptr, nullptr, nullptr, nullptr, InitSignalMap(env), napi_default_jsproperty, nullptr},
        {"_innerKill", nullptr, InnerKill, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_innerGetEnviron", nullptr, InnerGetEnviron, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_innerGetEnv", nullptr, InnerGetEnv, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_innerSetEnv", nullptr, InnerSetEnv, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_innerUnsetEnv", nullptr, InnerUnsetEnv, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_innerReadFd", nullptr, InnerReadFd, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_innerGetTitle", nullptr, InnerGetTitle, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_innerSetTitle", nullptr, InnerSetTitle, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
    };
    napi_value process = utils::CreateObject(env);
    napi_define_properties(env, process, desc.size(), desc.data());
    utils::SetUnenumerableProperty(env, global, "process", process);
    return global;
}
